05.2 精通自定义 View 之动画进阶——SVG 动画

返回自定义 View 目录

5.2.1 概述

SVG 全称是 Scalable Vector Graphics(可缩放矢量图形),即 SVG 是矢量图。与矢量图对应的是位图,Bitmap 就是位图,它由一个个像素点组成,当图片放大到一定大小时,就会出现马赛克现象,Photoshop 就是常用的位图处理软件。而矢量图则由一个个点组成,经过数学计算利用直线和曲线绘制而成,无论如何放大,都不会出现马赛克现象,Illustrator 就是常用的矢量图绘图软件。

SVG 与 Bitmap 相比有以下好处:

  • SVG 使用 XML 格式定义图形,可被非常多的工具读取和修改。
  • SVG 由点来存储,由计算机根据点信息绘图,不会失真,无须根据分辨率适配多套图标。
  • SVG 的占用空间明显比 Bitmap 小。如 500px X 500px 的图像,转成 SVG 后占用的空间大小是 20KB,而 PNG 图片则需要 732KB 的空间。
  • SVG 可以转换为 Path 路径,与 Path 动画相结合,可以形成丰富的动画。

对于 Android 5.0 以下的机型,可以通过引入 com.android.support:appcompat-v7:23.4.0 及以上版本进行支持。

Android 并没有对原生的 SVG 图像语法进行支持,而是以一种简化的方式对 SVG 进行兼容,也就是通过使用它的 path 标签,几乎可以实现 SVG 中的其他所有标签。这些东西可以通过工具来完成。

5.2.2 vector 标签与图像显示

res/drawable/svg.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:strokeColor="@color/colorAccent"/>
</vector>

效果图:

  • vector 标签:指定画布大小,上图蓝框区域。
  • path 标签:绘制路径,对应上图中的红色线段。
  • width & height 属性:表示该 SVG 图形的具体大小。
  • viewportWidth & viewportHeight 属性:表示 SVG 图形划分的比例。

width & height 类似于指定画布的大小,而 viewportWidth & viewportHeight 则是指将画布的宽、高分为多少个点,而 Path 中的点坐标都是以 viewportWidth & viewportHeight 的点数为坐标的,而不是 dp 值。此处将宽度 200dp 分为 100 个点,在高度 100dp 分为 50 个点,每个点有 2dp。而 path 中字母 M 表示 moveTo,字母 L 表示 lineTo,所以,这里代表从(50, 23) 到点 (100, 23) 画了一条线段。

1. path 标签

1)常用属性

  • android:name:声明一个标记,类似于 ID。
  • android:pathData:对 SVG 矢量图的描述。
  • android:strokeWidth:画笔的宽度
  • android:fillColor:填充颜色。
  • android:fillAlpha:填充颜色的透明度。
  • android:strokeColor:描边颜色。
  • android:strokeWidth:描边宽度。
  • android:strokeAlpha:描边透明度。
  • android:strokeLineJoin:用于指定折线拐角形状,取值有 miter(结合处为锐角)、round(结合处为圆弧)、bevel(结合处为直线)。
  • android:strokeLineCap:画出线条的终点的形状(线帽),取值有 butt(无线帽)、round(圆形线帽)、square(方形线帽)
  • android:strokeMiterLimit:设置斜角的上限。当 strokeLineJoin 为 “round” 或 “bevel” 时,该属性无效。

2)android:trimPathStart 属性
该属性用于指定路径从哪里开始,取值为 0~1,表示路径开始位置的百分比。取值为 0 时,表示从头开始;取值为 1 时,整条路径不可见。

1
2
3
4
5
6
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:trimPathStart="0.5"
android:strokeColor="@color/colorAccent"/>

灰色部分代表的是被删除的部分,实际上是不会显示出来的,这里只是为了展示效果,下同。

3)android:trimPathEnd 属性
该属性用于指定路径的结束位置,取值为 0~1,表是路径结束位置的百分比。取值为 1 时,路径正常结束;取值为 0 时,表示从开始位置就已经结束了,即整条路径不可见。

1
2
3
4
5
6
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:trimPathEnd="0.8"
android:strokeColor="@color/colorAccent"/>

4)android:trimPathOffset 属性
该属性用于指定路径的位移距离,取值 0~1。取值为 0 时,不位移;当取值 为 1 时,位移整条路径的长度。

1
2
3
4
5
6
7
8
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:trimPathStart="0.2"
android:trimPathEnd="0.4"
android:trimPathOffset="0.6"
android:strokeColor="@color/colorAccent"/>

5)android:pathData 属性
指定 SVG 图像的显示内容。

  • M = moveTo(M X,Y):将画笔移动到指定的坐标位置。
  • L = lineTo(L X,Y):画直线到指定的坐标位置。
  • H = horizontal lineTo(H X):画水平线到指定的 X 坐标位置。
  • V = vertical lineTo(V Y):画垂直线到指定的 Y 坐标位置。
  • C = curveTo(C X1,Y1,X2,Y2,ENDX,ENDY):三阶贝济埃曲线。
  • S = smooth curveTo(S X2,Y2,ENDX,ENDY):三阶贝济埃曲线。S 指令会将上一条指令的终点作为这条指令的起始点。
  • Q = quadratic Bezier curve(Q X,Y,ENDX,ENDY):二阶贝济埃曲线。
  • T = smooth quadratic Bezier curveTo(T ENDX,ENDY):映射前面路径后的终点。
  • A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线。
  • Z = closePath():关闭路径。

使用上面的指令时,需要注意的几点:

  • 坐标轴以(0,0)位中心,X轴水平向右,Y轴水平向下。
  • 所有指令大小写均可,大写绝对定位,参照全局坐标系,小写相对定位,参照父容器坐标系。
  • 指令和数据间的空格可以无视。
  • 同一指令出现多次可以用一个。

2. group 标签

group 标签用于定义一系列路径或者将 path 标签分组。具有以下常用属性。

  • android:name:组的名称,用于与动画相关联。
  • android:rotation:指定该组图像的旋转度数。
  • android:pivotX:定义缩放和旋转该组时的 X 参考点。
  • android:pivotY:定义缩放和旋转该组时的 Y 参考点。
  • android:scaleX:指定该组 X 轴缩放大小。
  • android:scaleY:指定该组 Y 轴缩放大小。
  • android:translateX:指定该组沿 X 轴平移的距离。
  • android:translateY:指定该组沿 Y 轴平移的距离。

示例:围绕画布中心旋转 90 度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<group
android:rotation="90"
android:pivotX="50"
android:pivotY="25">
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:strokeColor="@color/colorAccent"/>
</group>
</vector>

3. 制作 SVG 图像

方法一:设计软件
如果有绘图基础,可以直接使用 Illustrator 或在线 SVG 工具制作 SVG 图像(如 http://editor.method.ac/),或者通过 SVG 源文件下载网站下载后进行编辑。

方法二:Iconfont
有很多 Iconfont 开源网站,比如国内的阿里巴巴矢量图库,地址为 http://www.iconfont.cn/

4. 在 Android 中引入 SVG 图像

在 Android 中是不支持 SVG 图像解析的,我们必须将 SVG 图像转换为 vector 标签描述,这里同样有两种方法。

方法一:在线转换
This tool has been deprecated. Use official Vector Asset Studio instead.

方法二:Vector Asset Studio
Android Studio 2.0 及以上版本中支持创建 Vector 文件,如下图所示。

5. 示例

1)引入兼容包

1
2
3
compile 'com.android.support:appcompat-v7:23.4.0'
// 或使用 androidx
implementation 'androidx.appcompat:appcompat:1.0.2'

在项目的 build.gradle 脚本中添加对 Vector 兼容性的支持。

1
2
3
4
5
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}

2)生成 Vector 图像
使用前面例子中的一条横线的 Vector 图像(src/drawable/svg.xml)

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:strokeColor="@color/colorAccent"/>
</vector>

3)在 ImageView、ImageButton 中使用

1
2
3
4
5
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/svg"/>

在代码中设置

1
2
ImageView iv = findViewById(R.id.iv);
iv.setImageResource(R.drawable.svg);

本人测试使用 android:background=”@drawable/svg” 也是正常的。测试机型 Pixel XL,Android 7.1.2。

4)在 Button、RadioButton 中使用
Button 并不能直接通过 app:srcCompat 属性来使用 Vector 图像,而需要通过 selector 标签来使用(selector_svg.xml)

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/svg" android:state_pressed="true"/>
<item android:drawable="@drawable/svg"/>
</selector>

如果到这里并不能直接运行,需要把下面这段代码放在 Activity 的前面。

1
2
3
4
5
6
7
8
9
public class MainActivity extends AppCompatActivity {
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
@Override
public void onCreate(Bundle savedInstanceState) {
...
}
}

本人测试可以直接使用 android:background=”@drawable/svg” 并且不需要在 Activity 中加入上述代码 即可正常运行。测试机型 Pixel XL,Android 7.1.2。

5.2.3 动态 Vector

实现 Vector 动画,步骤如下:
1)使用上述 drawable/svg.xml
2)创建 animator/anim_trim_start.xml 文件

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathStart"
android:valueFrom="0"
android:valueTo="1"
android:duration="2000"/>

3)关联 Vector & Animator。drawable/animated_vector.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/svg">
<target
android:animation="@animator/anim_trim_start"
android:name="bar"/>
</animated-vector>

4)最后在代码中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
final ImageView imageView = findViewById(R.id.iv);
AnimatedVectorDrawableCompat compat = AnimatedVectorDrawableCompat.create(
MainActivity.this, R.drawable.animated_vector);
imageView.setImageDrawable(compat);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((Animatable) imageView.getDrawable()).start();
}
});
}
}

效果图如下所示:

5.2.4 示例:输入搜索动画

1. 准备 SVG 图像

res/drawable/svg.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="24dp"
android:viewportWidth="150"
android:viewportHeight="24">
<!-- 搜索图形 -->
<path
android:name="search"
android:pathData="M141,17 A9,9 0 1,1 142,16 L149,23"
android:strokeWidth="2"
android:strokeColor="@color/colorAccent"/>
<path
android:name="bar"
android:trimPathStart="1"
android:pathData="M0,23 L149,23"
android:strokeWidth="2"
android:strokeColor="@color/colorAccent"/>
</vector>

2. 准备动画

res/animator/anim_bar_trim_start.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathStart"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:duration="500"/>

res/animator/anim_search_trim_start.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:duration="500"/>

关联 Vector & Animator

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/svg">
<target
android:animation="@animator/anim_bar_trim_start"
android:name="bar"/>
<target
android:animation="@animator/anim_search_trim_start"
android:name="search"/>
</animated-vector>

3. 布局与开始动画

res/layout/act_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp" >
<EditText
android:id="@+id/edit"
android:layout_width="150dp"
android:layout_height="24dp"
android:hint="点击输入"
android:background="@null"/>
<ImageView
android:id="@+id/iv"
android:layout_width="150dp"
android:layout_height="24dp" />
</FrameLayout>

开始动画代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
final ImageView imageView = findViewById(R.id.iv);
// 将焦点放在 ImageView 上
imageView.setFocusable(true);
imageView.setFocusableInTouchMode(true);
imageView.requestFocus();
imageView.requestFocusFromTouch();
EditText editText = findViewById(R.id.edit);
editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
AnimatedVectorDrawableCompat compat = AnimatedVectorDrawableCompat.create(
MainActivity.this, R.drawable.animated_vector);
imageView.setImageDrawable(compat);
((Animatable) imageView.getDrawable()).start();
}
});
}
}